home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / connections.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  27.5 KB  |  766 lines

  1. """
  2. Fetches connection data (IP addresses and ports) associated with a given
  3. process. This sort of data can be retrieved via a variety of common *nix
  4. utilities:
  5. - netstat   netstat -np | grep "ESTABLISHED <pid>/<process>"
  6. - sockstat  sockstat | egrep "<process> *<pid>.*ESTABLISHED"
  7. - lsof      lsof -wnPi | egrep "^<process> *<pid>.*((UDP.*)|(\(ESTABLISHED\)))"
  8. - ss        ss -nptu | grep "ESTAB.*\"<process>\",<pid>"
  9.  
  10. all queries dump its stderr (directing it to /dev/null). Results include UDP
  11. and established TCP connections.
  12.  
  13. FreeBSD lacks support for the needed netstat flags and has a completely
  14. different program for 'ss'. However, lsof works and there's a couple other
  15. options that perform even better (thanks to Fabian Keil and Hans Schnehl):
  16. - sockstat    sockstat -4c | grep '<process> *<pid>'
  17. - procstat    procstat -f <pid> | grep TCP | grep -v 0.0.0.0:0
  18. """
  19.  
  20. import os
  21. import time
  22. import threading
  23.  
  24. from util import enum, log, procTools, sysTools
  25.  
  26. # enums for connection resolution utilities
  27. Resolver = enum.Enum(("PROC", "proc"),
  28.                      ("NETSTAT", "netstat"),
  29.                      ("SS", "ss"),
  30.                      ("LSOF", "lsof"),
  31.                      ("SOCKSTAT", "sockstat"),
  32.                      ("BSD_SOCKSTAT", "sockstat (bsd)"),
  33.                      ("BSD_PROCSTAT", "procstat (bsd)"))
  34.  
  35. # If true this provides new instantiations for resolvers if the old one has
  36. # been stopped. This can make it difficult ensure all threads are terminated
  37. # when accessed concurrently.
  38. RECREATE_HALTED_RESOLVERS = False
  39.  
  40. # formatted strings for the commands to be executed with the various resolvers
  41. # options are:
  42. # n = prevents dns lookups, p = include process
  43. # output:
  44. # tcp  0  0  127.0.0.1:9051  127.0.0.1:53308  ESTABLISHED 9912/tor
  45. # *note: bsd uses a different variant ('-t' => '-p tcp', but worse an
  46. #   equivilant -p doesn't exist so this can't function)
  47. RUN_NETSTAT = "netstat -np | grep \"ESTABLISHED %s/%s\""
  48.  
  49. # n = numeric ports, p = include process, t = tcp sockets, u = udp sockets
  50. # output:
  51. # ESTAB  0  0  127.0.0.1:9051  127.0.0.1:53308  users:(("tor",9912,20))
  52. # *note: under freebsd this command belongs to a spreadsheet program
  53. RUN_SS = "ss -nptu | grep \"ESTAB.*\\\"%s\\\",%s\""
  54.  
  55. # n = prevent dns lookups, P = show port numbers (not names), i = ip only,
  56. # -w = no warnings
  57. # output:
  58. # tor  3873  atagar  45u  IPv4  40994  0t0  TCP 10.243.55.20:45724->194.154.227.109:9001 (ESTABLISHED)
  59. # oddly, using the -p flag via:
  60. # lsof      lsof -nPi -p <pid> | grep "^<process>.*(ESTABLISHED)"
  61. # is much slower (11-28% in tests I ran)
  62. RUN_LSOF = "lsof -wnPi | egrep \"^%s *%s.*((UDP.*)|(\\(ESTABLISHED\\)))\""
  63.  
  64. # output:
  65. # atagar  tor  3475  tcp4  127.0.0.1:9051  127.0.0.1:38942  ESTABLISHED
  66. # *note: this isn't available by default under ubuntu
  67. RUN_SOCKSTAT = "sockstat | egrep \"%s *%s.*ESTABLISHED\""
  68.  
  69. RUN_BSD_SOCKSTAT = "sockstat -4c | grep '%s *%s'"
  70. RUN_BSD_PROCSTAT = "procstat -f %s | grep TCP | grep -v 0.0.0.0:0"
  71.  
  72. RESOLVERS = []                      # connection resolvers available via the singleton constructor
  73. RESOLVER_FAILURE_TOLERANCE = 3      # number of subsequent failures before moving on to another resolver
  74. RESOLVER_SERIAL_FAILURE_MSG = "Unable to query connections with %s, trying %s"
  75. RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed"
  76. CONFIG = {"queries.connections.minRate": 5,
  77.           "log.connResolverOptions": log.INFO,
  78.           "log.connLookupFailed": log.INFO,
  79.           "log.connLookupFailover": log.NOTICE,
  80.           "log.connLookupAbandon": log.NOTICE,
  81.           "log.connLookupRateGrowing": None,
  82.           "log.configEntryTypeError": log.NOTICE}
  83.  
  84. PORT_USAGE = {}
  85.  
  86. def loadConfig(config):
  87.   config.update(CONFIG)
  88.   
  89.   for configKey in config.getKeys():
  90.     # fetches any port.label.* values
  91.     if configKey.startswith("port.label."):
  92.       portEntry = configKey[11:]
  93.       purpose = config.get(configKey)
  94.       
  95.       divIndex = portEntry.find("-")
  96.       if divIndex == -1:
  97.         # single port
  98.         if portEntry.isdigit():
  99.           PORT_USAGE[portEntry] = purpose
  100.         else:
  101.           msg = "Port value isn't numeric for entry: %s" % configKey
  102.           log.log(CONFIG["log.configEntryTypeError"], msg)
  103.       else:
  104.         try:
  105.           # range of ports (inclusive)
  106.           minPort = int(portEntry[:divIndex])
  107.           maxPort = int(portEntry[divIndex + 1:])
  108.           if minPort > maxPort: raise ValueError()
  109.           
  110.           for port in range(minPort, maxPort + 1):
  111.             PORT_USAGE[str(port)] = purpose
  112.         except ValueError:
  113.           msg = "Unable to parse port range for entry: %s" % configKey
  114.           log.log(CONFIG["log.configEntryTypeError"], msg)
  115.  
  116. def isValidIpAddress(ipStr):
  117.   """
  118.   Returns true if input is a valid IPv4 address, false otherwise.
  119.   """
  120.   
  121.   # checks if theres four period separated values
  122.   if not ipStr.count(".") == 3: return False
  123.   
  124.   # checks that each value in the octet are decimal values between 0-255
  125.   for ipComp in ipStr.split("."):
  126.     if not ipComp.isdigit() or int(ipComp) < 0 or int(ipComp) > 255:
  127.       return False
  128.   
  129.   return True
  130.  
  131. def isIpAddressPrivate(ipAddr):
  132.   """
  133.   Provides true if the IP address belongs on the local network or belongs to
  134.   loopback, false otherwise. These include:
  135.   Private ranges: 10.*, 172.16.* - 172.31.*, 192.168.*
  136.   Loopback: 127.*
  137.   
  138.   Arguments:
  139.     ipAddr - IP address to be checked
  140.   """
  141.   
  142.   # checks for any of the simple wildcard ranges
  143.   if ipAddr.startswith("10.") or ipAddr.startswith("192.168.") or ipAddr.startswith("127."):
  144.     return True
  145.   
  146.   # checks for the 172.16.* - 172.31.* range
  147.   if ipAddr.startswith("172.") and ipAddr.count(".") == 3:
  148.     secondOctet = ipAddr[4:ipAddr.find(".", 4)]
  149.     
  150.     if secondOctet.isdigit() and int(secondOctet) >= 16 and int(secondOctet) <= 31:
  151.       return True
  152.   
  153.   return False
  154.  
  155. def ipToInt(ipAddr):
  156.   """
  157.   Provides an integer representation of the ip address, suitable for sorting.
  158.   
  159.   Arguments:
  160.     ipAddr - ip address to be converted
  161.   """
  162.   
  163.   total = 0
  164.   
  165.   for comp in ipAddr.split("."):
  166.     total *= 255
  167.     total += int(comp)
  168.   
  169.   return total
  170.  
  171. def getPortUsage(port):
  172.   """
  173.   Provides the common use of a given port. If no useage is known then this
  174.   provides None.
  175.   
  176.   Arguments:
  177.     port - port number to look up
  178.   """
  179.   
  180.   return PORT_USAGE.get(port)
  181.  
  182. def getResolverCommand(resolutionCmd, processName, processPid = ""):
  183.   """
  184.   Provides the command that would be processed for the given resolver type.
  185.   This raises a ValueError if either the resolutionCmd isn't recognized or a
  186.   pid was requited but not provided.
  187.   
  188.   Arguments:
  189.     resolutionCmd - command to use in resolving the address
  190.     processName   - name of the process for which connections are fetched
  191.     processPid    - process ID (this helps improve accuracy)
  192.   """
  193.   
  194.   if not processPid:
  195.     # the pid is required for procstat resolution
  196.     if resolutionCmd == Resolver.BSD_PROCSTAT:
  197.       raise ValueError("procstat resolution requires a pid")
  198.     
  199.     # if the pid was undefined then match any in that field
  200.     processPid = "[0-9]*"
  201.   
  202.   if resolutionCmd == Resolver.PROC: return ""
  203.   elif resolutionCmd == Resolver.NETSTAT: return RUN_NETSTAT % (processPid, processName)
  204.   elif resolutionCmd == Resolver.SS: return RUN_SS % (processName, processPid)
  205.   elif resolutionCmd == Resolver.LSOF: return RUN_LSOF % (processName, processPid)
  206.   elif resolutionCmd == Resolver.SOCKSTAT: return RUN_SOCKSTAT % (processName, processPid)
  207.   elif resolutionCmd == Resolver.BSD_SOCKSTAT: return RUN_BSD_SOCKSTAT % (processName, processPid)
  208.   elif resolutionCmd == Resolver.BSD_PROCSTAT: return RUN_BSD_PROCSTAT % processPid
  209.   else: raise ValueError("Unrecognized resolution type: %s" % resolutionCmd)
  210.  
  211. def getConnections(resolutionCmd, processName, processPid = ""):
  212.   """
  213.   Retrieves a list of the current connections for a given process, providing a
  214.   tuple list of the form:
  215.   [(local_ipAddr1, local_port1, foreign_ipAddr1, foreign_port1), ...]
  216.   this raises an IOError if no connections are available or resolution fails
  217.   (in most cases these appear identical). Common issues include:
  218.     - insufficient permissions
  219.     - resolution command is unavailable
  220.     - usage of the command is non-standard (particularly an issue for BSD)
  221.   
  222.   Arguments:
  223.     resolutionCmd - command to use in resolving the address
  224.     processName   - name of the process for which connections are fetched
  225.     processPid    - process ID (this helps improve accuracy)
  226.   """
  227.   
  228.   if resolutionCmd == Resolver.PROC:
  229.     # Attempts resolution via checking the proc contents.
  230.     if not processPid:
  231.       raise ValueError("proc resolution requires a pid")
  232.     
  233.     try:
  234.       return procTools.getConnections(processPid)
  235.     except Exception, exc:
  236.       raise IOError(str(exc))
  237.   else:
  238.     # Queries a resolution utility (netstat, lsof, etc). This raises an
  239.     # IOError if the command fails or isn't available.
  240.     cmd = getResolverCommand(resolutionCmd, processName, processPid)
  241.     results = sysTools.call(cmd)
  242.     
  243.     if not results: raise IOError("No results found using: %s" % cmd)
  244.     
  245.     # parses results for the resolution command
  246.     conn = []
  247.     for line in results:
  248.       if resolutionCmd == Resolver.LSOF:
  249.         # Different versions of lsof have different numbers of columns, so
  250.         # stripping off the optional 'established' entry so we can just use
  251.         # the last one.
  252.         comp = line.replace("(ESTABLISHED)", "").strip().split()
  253.       else: comp = line.split()
  254.       
  255.       if resolutionCmd == Resolver.NETSTAT:
  256.         localIp, localPort = comp[3].split(":")
  257.         foreignIp, foreignPort = comp[4].split(":")
  258.       elif resolutionCmd == Resolver.SS:
  259.         localIp, localPort = comp[4].split(":")
  260.         foreignIp, foreignPort = comp[5].split(":")
  261.       elif resolutionCmd == Resolver.LSOF:
  262.         local, foreign = comp[-1].split("->")
  263.         localIp, localPort = local.split(":")
  264.         foreignIp, foreignPort = foreign.split(":")
  265.       elif resolutionCmd == Resolver.SOCKSTAT:
  266.         localIp, localPort = comp[4].split(":")
  267.         foreignIp, foreignPort = comp[5].split(":")
  268.       elif resolutionCmd == Resolver.BSD_SOCKSTAT:
  269.         localIp, localPort = comp[5].split(":")
  270.         foreignIp, foreignPort = comp[6].split(":")
  271.       elif resolutionCmd == Resolver.BSD_PROCSTAT:
  272.         localIp, localPort = comp[9].split(":")
  273.         foreignIp, foreignPort = comp[10].split(":")
  274.       
  275.       conn.append((localIp, localPort, foreignIp, foreignPort))
  276.     
  277.     return conn
  278.  
  279. def isResolverAlive(processName, processPid = ""):
  280.   """
  281.   This provides true if a singleton resolver instance exists for the given
  282.   process/pid combination, false otherwise.
  283.   
  284.   Arguments:
  285.     processName - name of the process being checked
  286.     processPid  - pid of the process being checked, if undefined this matches
  287.                   against any resolver with the process name
  288.   """
  289.   
  290.   for resolver in RESOLVERS:
  291.     if not resolver._halt and resolver.processName == processName and (not processPid or resolver.processPid == processPid):
  292.       return True
  293.   
  294.   return False
  295.  
  296. def getResolver(processName, processPid = "", alias=None):
  297.   """
  298.   Singleton constructor for resolver instances. If a resolver already exists
  299.   for the process then it's returned. Otherwise one is created and started.
  300.   
  301.   Arguments:
  302.     processName - name of the process being resolved
  303.     processPid  - pid of the process being resolved, if undefined this matches
  304.                   against any resolver with the process name
  305.     alias       - alternative handle under which the resolver can be requested
  306.   """
  307.   
  308.   # check if one's already been created
  309.   requestHandle = alias if alias else processName
  310.   haltedIndex = -1 # old instance of this resolver with the _halt flag set
  311.   for i in range(len(RESOLVERS)):
  312.     resolver = RESOLVERS[i]
  313.     if resolver.handle == requestHandle and (not processPid or resolver.processPid == processPid):
  314.       if resolver._halt and RECREATE_HALTED_RESOLVERS: haltedIndex = i
  315.       else: return resolver
  316.   
  317.   # make a new resolver
  318.   r = ConnectionResolver(processName, processPid, handle = requestHandle)
  319.   r.start()
  320.   
  321.   # overwrites halted instance of this resolver if it exists, otherwise append
  322.   if haltedIndex == -1: RESOLVERS.append(r)
  323.   else: RESOLVERS[haltedIndex] = r
  324.   return r
  325.  
  326. def getSystemResolvers(osType = None):
  327.   """
  328.   Provides the types of connection resolvers available on this operating
  329.   system.
  330.   
  331.   Arguments:
  332.     osType - operating system type, fetched from the os module if undefined
  333.   """
  334.   
  335.   if osType == None: osType = os.uname()[0]
  336.   
  337.   if osType == "FreeBSD":
  338.     resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF]
  339.   elif osType in ("OpenBSD", "Darwin"):
  340.     resolvers = [Resolver.LSOF]
  341.   else:
  342.     resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS]
  343.   
  344.   # proc resolution, by far, outperforms the others so defaults to this is able
  345.   if procTools.isProcAvailable():
  346.     resolvers = [Resolver.PROC] + resolvers
  347.   
  348.   return resolvers
  349.  
  350. class ConnectionResolver(threading.Thread):
  351.   """
  352.   Service that periodically queries for a process' current connections. This
  353.   provides several benefits over on-demand queries:
  354.   - queries are non-blocking (providing cached results)
  355.   - falls back to use different resolution methods in case of repeated failures
  356.   - avoids overly frequent querying of connection data, which can be demanding
  357.     in terms of system resources
  358.   
  359.   Unless an overriding method of resolution is requested this defaults to
  360.   choosing a resolver the following way:
  361.   
  362.   - Checks the current PATH to determine which resolvers are available. This
  363.     uses the first of the following that's available:
  364.       netstat, ss, lsof (picks netstat if none are found)
  365.   
  366.   - Attempts to resolve using the selection. Single failures are logged at the
  367.     INFO level, and a series of failures at NOTICE. In the later case this
  368.     blacklists the resolver, moving on to the next. If all resolvers fail this
  369.     way then resolution's abandoned and logs a WARN message.
  370.   
  371.   The time between resolving connections, unless overwritten, is set to be
  372.   either five seconds or ten times the runtime of the resolver (whichever is
  373.   larger). This is to prevent systems either strapped for resources or with a
  374.   vast number of connections from being burdened too heavily by this daemon.
  375.   
  376.   Parameters:
  377.     processName       - name of the process being resolved
  378.     processPid        - pid of the process being resolved
  379.     resolveRate       - minimum time between resolving connections (in seconds,
  380.                         None if using the default)
  381.     * defaultRate     - default time between resolving connections
  382.     lastLookup        - time connections were last resolved (unix time, -1 if
  383.                         no resolutions have yet been successful)
  384.     overwriteResolver - method of resolution (uses default if None)
  385.     * defaultResolver - resolver used by default (None if all resolution
  386.                         methods have been exhausted)
  387.     resolverOptions   - resolvers to be cycled through (differ by os)
  388.     
  389.     * read-only
  390.   """
  391.   
  392.   def __init__(self, processName, processPid = "", resolveRate = None, handle = None):
  393.     """
  394.     Initializes a new resolver daemon. When no longer needed it's suggested
  395.     that this is stopped.
  396.     
  397.     Arguments:
  398.       processName - name of the process being resolved
  399.       processPid  - pid of the process being resolved
  400.       resolveRate - time between resolving connections (in seconds, None if
  401.                     chosen dynamically)
  402.       handle      - name used to query this resolver, this is the processName
  403.                     if undefined
  404.     """
  405.     
  406.     threading.Thread.__init__(self)
  407.     self.setDaemon(True)
  408.     
  409.     self.processName = processName
  410.     self.processPid = processPid
  411.     self.resolveRate = resolveRate
  412.     self.handle = handle if handle else processName
  413.     self.defaultRate = CONFIG["queries.connections.minRate"]
  414.     self.lastLookup = -1
  415.     self.overwriteResolver = None
  416.     self.defaultResolver = Resolver.PROC
  417.     
  418.     osType = os.uname()[0]
  419.     self.resolverOptions = getSystemResolvers(osType)
  420.     
  421.     log.log(CONFIG["log.connResolverOptions"], "Operating System: %s, Connection Resolvers: %s" % (osType, ", ".join(self.resolverOptions)))
  422.     
  423.     # sets the default resolver to be the first found in the system's PATH
  424.     # (left as netstat if none are found)
  425.     for resolver in self.resolverOptions:
  426.       # Resolver strings correspond to their command with the exception of bsd
  427.       # resolvers.
  428.       resolverCmd = resolver.replace(" (bsd)", "")
  429.       
  430.       if resolver == Resolver.PROC or sysTools.isAvailable(resolverCmd):
  431.         self.defaultResolver = resolver
  432.         break
  433.     
  434.     self._connections = []        # connection cache (latest results)
  435.     self._resolutionCounter = 0   # number of successful connection resolutions
  436.     self._isPaused = False
  437.     self._halt = False            # terminates thread if true
  438.     self._cond = threading.Condition()  # used for pausing the thread
  439.     self._subsiquentFailures = 0  # number of failed resolutions with the default in a row
  440.     self._resolverBlacklist = []  # resolvers that have failed to resolve
  441.     
  442.     # Number of sequential times the threshold rate's been too low. This is to
  443.     # avoid having stray spikes up the rate.
  444.     self._rateThresholdBroken = 0
  445.   
  446.   def getOverwriteResolver(self):
  447.     """
  448.     Provides the resolver connection resolution is forced to use. This returns
  449.     None if it's dynamically determined.
  450.     """
  451.     
  452.     return self.overwriteResolver
  453.      
  454.   def setOverwriteResolver(self, overwriteResolver):
  455.     """
  456.     Sets the resolver used for connection resolution, if None then this is
  457.     automatically determined based on what is available.
  458.     
  459.     Arguments:
  460.       overwriteResolver - connection resolver to be used
  461.     """
  462.     
  463.     self.overwriteResolver = overwriteResolver
  464.   
  465.   def run(self):
  466.     while not self._halt:
  467.       minWait = self.resolveRate if self.resolveRate else self.defaultRate
  468.       timeSinceReset = time.time() - self.lastLookup
  469.       
  470.       if self._isPaused or timeSinceReset < minWait:
  471.         sleepTime = max(0.2, minWait - timeSinceReset)
  472.         
  473.         self._cond.acquire()
  474.         if not self._halt: self._cond.wait(sleepTime)
  475.         self._cond.release()
  476.         
  477.         continue # done waiting, try again
  478.       
  479.       isDefault = self.overwriteResolver == None
  480.       resolver = self.defaultResolver if isDefault else self.overwriteResolver
  481.       
  482.       # checks if there's nothing to resolve with
  483.       if not resolver:
  484.         self.lastLookup = time.time() # avoids a busy wait in this case
  485.         continue
  486.       
  487.       try:
  488.         resolveStart = time.time()
  489.         connResults = getConnections(resolver, self.processName, self.processPid)
  490.         lookupTime = time.time() - resolveStart
  491.         
  492.         self._connections = connResults
  493.         self._resolutionCounter += 1
  494.         
  495.         newMinDefaultRate = 100 * lookupTime
  496.         if self.defaultRate < newMinDefaultRate:
  497.           if self._rateThresholdBroken >= 3:
  498.             # adding extra to keep the rate from frequently changing
  499.             self.defaultRate = newMinDefaultRate + 0.5
  500.             
  501.             msg = "connection lookup time increasing to %0.1f seconds per call" % self.defaultRate
  502.             log.log(CONFIG["log.connLookupRateGrowing"], msg)
  503.           else: self._rateThresholdBroken += 1
  504.         else: self._rateThresholdBroken = 0
  505.         
  506.         if isDefault: self._subsiquentFailures = 0
  507.       except (ValueError, IOError), exc:
  508.         # this logs in a couple of cases:
  509.         # - special failures noted by getConnections (most cases are already
  510.         # logged via sysTools)
  511.         # - note fail-overs for default resolution methods
  512.         if str(exc).startswith("No results found using:"):
  513.           log.log(CONFIG["log.connLookupFailed"], str(exc))
  514.         
  515.         if isDefault:
  516.           self._subsiquentFailures += 1
  517.           
  518.           if self._subsiquentFailures >= RESOLVER_FAILURE_TOLERANCE:
  519.             # failed several times in a row - abandon resolver and move on to another
  520.             self._resolverBlacklist.append(resolver)
  521.             self._subsiquentFailures = 0
  522.             
  523.             # pick another (non-blacklisted) resolver
  524.             newResolver = None
  525.             for r in self.resolverOptions:
  526.               if not r in self._resolverBlacklist:
  527.                 newResolver = r
  528.                 break
  529.             
  530.             if newResolver:
  531.               # provide notice that failures have occurred and resolver is changing
  532.               msg = RESOLVER_SERIAL_FAILURE_MSG % (resolver, newResolver)
  533.               log.log(CONFIG["log.connLookupFailover"], msg)
  534.             else:
  535.               # exhausted all resolvers, give warning
  536.               log.log(CONFIG["log.connLookupAbandon"], RESOLVER_FINAL_FAILURE_MSG)
  537.             
  538.             self.defaultResolver = newResolver
  539.       finally:
  540.         self.lastLookup = time.time()
  541.   
  542.   def getConnections(self):
  543.     """
  544.     Provides the last queried connection results, an empty list if resolver
  545.     has been halted.
  546.     """
  547.     
  548.     if self._halt: return []
  549.     else: return list(self._connections)
  550.   
  551.   def getResolutionCount(self):
  552.     """
  553.     Provides the number of successful resolutions so far. This can be used to
  554.     determine if the connection results are new for the caller or not.
  555.     """
  556.     
  557.     return self._resolutionCounter
  558.   
  559.   def getPid(self):
  560.     """
  561.     Provides the pid used to narrow down connection resolution. This is an
  562.     empty string if undefined.
  563.     """
  564.     
  565.     return self.processPid
  566.   
  567.   def setPid(self, processPid):
  568.     """
  569.     Sets the pid used to narrow down connection resultions.
  570.     
  571.     Arguments:
  572.       processPid - pid for the process we're fetching connections for
  573.     """
  574.     
  575.     self.processPid = processPid
  576.   
  577.   def setPaused(self, isPause):
  578.     """
  579.     Allows or prevents further connection resolutions (this still makes use of
  580.     cached results).
  581.     
  582.     Arguments:
  583.       isPause - puts a freeze on further resolutions if true, allows them to
  584.                 continue otherwise
  585.     """
  586.     
  587.     if isPause == self._isPaused: return
  588.     self._isPaused = isPause
  589.   
  590.   def stop(self):
  591.     """
  592.     Halts further resolutions and terminates the thread.
  593.     """
  594.     
  595.     self._cond.acquire()
  596.     self._halt = True
  597.     self._cond.notifyAll()
  598.     self._cond.release()
  599.  
  600. class AppResolver:
  601.   """
  602.   Provides the names and pids of appliations attached to the given ports. This
  603.   stops attempting to query if it fails three times without successfully
  604.   getting lsof results.
  605.   """
  606.   
  607.   def __init__(self, scriptName = "python"):
  608.     """
  609.     Constructs a resolver instance.
  610.     
  611.     Arguments:
  612.       scriptName - name by which to all our own entries
  613.     """
  614.     
  615.     self.scriptName = scriptName
  616.     self.queryResults = {}
  617.     self.resultsLock = threading.RLock()
  618.     self._cond = threading.Condition()  # used for pausing when waiting for results
  619.     self.isResolving = False  # flag set if we're in the process of making a query
  620.     self.failureCount = 0     # -1 if we've made a successful query
  621.   
  622.   def getResults(self, maxWait=0):
  623.     """
  624.     Provides the last queried results. If we're in the process of making a
  625.     query then we can optionally block for a time to see if it finishes.
  626.     
  627.     Arguments:
  628.       maxWait - maximum second duration to block on getting results before
  629.                 returning
  630.     """
  631.     
  632.     self._cond.acquire()
  633.     if self.isResolving and maxWait > 0:
  634.       self._cond.wait(maxWait)
  635.     self._cond.release()
  636.     
  637.     self.resultsLock.acquire()
  638.     results = dict(self.queryResults)
  639.     self.resultsLock.release()
  640.     
  641.     return results
  642.   
  643.   def resolve(self, ports):
  644.     """
  645.     Queues the given listing of ports to be resolved. This clears the last set
  646.     of results when completed.
  647.     
  648.     Arguments:
  649.       ports - list of ports to be resolved to applications
  650.     """
  651.     
  652.     if self.failureCount < 3:
  653.       self.isResolving = True
  654.       t = threading.Thread(target = self._queryApplications, kwargs = {"ports": ports})
  655.       t.setDaemon(True)
  656.       t.start()
  657.   
  658.   def _queryApplications(self, ports=[]):
  659.     """
  660.     Performs an lsof lookup on the given ports to get the command/pid tuples.
  661.     
  662.     Arguments:
  663.       ports - list of ports to be resolved to applications
  664.     """
  665.     
  666.     # atagar@fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277
  667.     # COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
  668.     # tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
  669.     # tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
  670.     # python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
  671.     # python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
  672.     
  673.     if not ports:
  674.       self.resultsLock.acquire()
  675.       self.queryResults = {}
  676.       self.isResolving = False
  677.       self.resultsLock.release()
  678.       
  679.       # wakes threads waiting on results
  680.       self._cond.acquire()
  681.       self._cond.notifyAll()
  682.       self._cond.release()
  683.       
  684.       return
  685.     
  686.     results = {}
  687.     lsofArgs = []
  688.     
  689.     # Uses results from the last query if we have any, otherwise appends the
  690.     # port to the lsof command. This has the potential for persisting dirty
  691.     # results but if we're querying by the dynamic port on the local tcp
  692.     # connections then this should be very rare (and definitely worth the
  693.     # chance of being able to skip an lsof query altogether).
  694.     for port in ports:
  695.       if port in self.queryResults:
  696.         results[port] = self.queryResults[port]
  697.       else: lsofArgs.append("-i tcp:%s" % port)
  698.     
  699.     if lsofArgs:
  700.       lsofResults = sysTools.call("lsof -nP " + " ".join(lsofArgs))
  701.     else: lsofResults = None
  702.     
  703.     if not lsofResults and self.failureCount != -1:
  704.       # lsof query failed and we aren't yet sure if it's possible to
  705.       # successfully get results on this platform
  706.       self.failureCount += 1
  707.       self.isResolving = False
  708.       return
  709.     elif lsofResults:
  710.       # (iPort, oPort) tuple for our own process, if it was fetched
  711.       ourConnection = None
  712.       
  713.       for line in lsofResults:
  714.         lineComp = line.split()
  715.         
  716.         if len(lineComp) == 10 and lineComp[9] == "(ESTABLISHED)":
  717.           cmd, pid, _, _, _, _, _, _, portMap, _ = lineComp
  718.           
  719.           if "->" in portMap:
  720.             iPort, oPort = portMap.split("->")
  721.             iPort = iPort.split(":")[1]
  722.             oPort = oPort.split(":")[1]
  723.             
  724.             # entry belongs to our own process
  725.             if pid == str(os.getpid()):
  726.               cmd = self.scriptName
  727.               ourConnection = (iPort, oPort)
  728.             
  729.             if iPort.isdigit() and oPort.isdigit():
  730.               newEntry = (iPort, oPort, cmd, pid)
  731.               
  732.               # adds the entry under the key of whatever we queried it with
  733.               # (this might be both the inbound _and_ outbound ports)
  734.               for portMatch in (iPort, oPort):
  735.                 if portMatch in ports:
  736.                   if portMatch in results:
  737.                     results[portMatch].append(newEntry)
  738.                   else: results[portMatch] = [newEntry]
  739.       
  740.       # making the lsof call generated an extraneous sh entry for our own connection
  741.       if ourConnection:
  742.         for ourPort in ourConnection:
  743.           if ourPort in results:
  744.             shIndex = None
  745.             
  746.             for i in range(len(results[ourPort])):
  747.               if results[ourPort][i][2] == "sh":
  748.                 shIndex = i
  749.                 break
  750.             
  751.             if shIndex != None:
  752.               del results[ourPort][shIndex]
  753.     
  754.     self.resultsLock.acquire()
  755.     self.failureCount = -1
  756.     self.queryResults = results
  757.     self.isResolving = False
  758.     self.resultsLock.release()
  759.     
  760.     # wakes threads waiting on results
  761.     self._cond.acquire()
  762.     self._cond.notifyAll()
  763.     self._cond.release()
  764.  
  765.